/**
 * ForEach.java.
 * <p>
 * Purpose:
 * <p>
 * Description:
 * <p>
 * History:
 * 12:48:11 PM Oct 22, 2014, Created by jumperchen
 * <p>
 * Copyright (C) 2014 Potix Corporation. All Rights Reserved.
 */
package org.zkoss.zuti.zul;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.zkoss.zuti.ForEachRenderer;
import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.sys.IntPropertyAccess;
import org.zkoss.zk.ui.sys.ObjectPropertyAccess;
import org.zkoss.zk.ui.sys.PropertyAccess;
import org.zkoss.zk.ui.sys.ShadowElementsCtrl;
import org.zkoss.zk.ui.sys.StringPropertyAccess;
import org.zkoss.zk.ui.util.Template;
import org.zkoss.zul.ListModel;
import org.zkoss.zul.ListModelArray;

/**
 * The basic iteration tag, accepting many different collection types and
 * supporting sub-setting and other functionality like JSTL's <tt>forEach</tt>
 *
 * @author jumperchen
 * @since 8.0.0
 */
public class ForEach extends TemplateBasedShadowElement {

    private static final long serialVersionUID = 2014102212473126L;
    private AuxInfo _auxinf = null;
    private ForEachConverter _conv = new ForEachConverter();

    public void setItems(Object items) {
        _dirtyBinding = true; // always update because the items may be a collection type

        // no need to check auinfo.items whether the same as items, if the items is a huge data,
        // it will take a lot of time to check.
        initAuxInfo().items = items;
    }

    public Object getItems() {
        return _auxinf != null ? _auxinf.items : null;
    }

    public void setBegin(int begin) {
        if (begin < 0)
            throw new IllegalArgumentException("'begin' < 0 [" + begin + "]");
        if (!Objects.equals(_auxinf != null ? _auxinf.begin : null, begin)) {
            initAuxInfo().begin = begin;
            _dirtyBinding = true;
        }
    }

    public int getBegin() {
        return _auxinf != null ? _auxinf.begin : 0;
    }

    public void setEnd(int end) {
        if (end < 0)
            throw new IllegalArgumentException("'end' < 0 [" + end + "]");
        if (!Objects.equals(_auxinf != null ? _auxinf.end : null, end)) {
            initAuxInfo().end = end;
            _dirtyBinding = true;
        }
    }

    public int getEnd() {
        if (_auxinf != null && _auxinf.end > -1)
            return _auxinf.end;
        int size = getItemsSize();
        return size > 0 ? size - 1 : 0;
    }

    private int getItemsSize() {
        Object items = getItems();
        if (items instanceof Collection) {
            return ((Collection) items).size();
        } else if (items instanceof Map) {
            return ((Map) items).size();
        } else if (items instanceof ListModelArray<?>) {
            return (((ListModelArray<?>) items).getInnerArray()).length;
        } else if (items instanceof Object[]) {
            return ((Object[]) items).length;
        } else if ((items instanceof Class) && Enum.class.isAssignableFrom((Class) items)) {
            return ((Class) items).getEnumConstants().length;
        } else {
            return 0;
        }
    }

    public void setStep(int step) {
        if (step <= 0)
            throw new IllegalArgumentException("'step' <= 0 [" + step + "]");
        if (!Objects.equals(_auxinf != null ? _auxinf.step : null, step)) {
            initAuxInfo().step = step;
            _dirtyBinding = true;
        }
    }

    public int getStep() {
        return _auxinf != null ? _auxinf.step : 1;
    }

    public void setVar(String var) {
        if (!Objects.equals(_auxinf != null ? _auxinf.var : null, var)) {
            initAuxInfo().var = var;
            _dirtyBinding = true;
        }
    }

    public String getVar() {
        return _auxinf != null ? _auxinf.var : "each";
    }

    public void setVarStatus(String varStatus) {
        if (!Objects.equals(_auxinf != null ? _auxinf.varStatus : null, varStatus)) {
            initAuxInfo().varStatus = varStatus;
            _dirtyBinding = true;
        }
    }

    public String getVarStatus() {
        return _auxinf != null ? _auxinf.varStatus : "forEachStatus";
    }

    public ForEachConverter getDataConverter() {
        return _conv;
    }

    private final AuxInfo initAuxInfo() {
        if (_auxinf == null)
            _auxinf = new AuxInfo();
        return _auxinf;
    }

    private static class AuxInfo implements java.io.Serializable, Cloneable {
        private Object items;
        private int begin = 0;
        private int end = -1; // if -1, return from items
        private int step = 1;
        private String var = "each"; // backward compatible for old ZK version
        private String varStatus = "forEachStatus"; // backward compatible for
        // old ZK version

        public Object clone() {
            try {
                return super.clone();
            } catch (CloneNotSupportedException e) {
                throw new InternalError();
            }
        }
    }

    protected boolean isEffective() {
        if (getItems() != null) {
            return true;
        }
        if (_auxinf == null)
            return false;
        return _auxinf.begin <= _auxinf.end;
    }

    protected ForEachListDataListener _dataListener;

    // Bug fixed for ZK-2837, we need to unlisten the data listener
    private void initDataListener(ListModel model, Component host) {
        if (_dataListener == null) {
            _dataListener = new ForEachListDataListener(this, host);
        } else {
            model.removeListDataListener(_dataListener);
            _dataListener = new ForEachListDataListener(this, host);
        }
        model.addListDataListener(_dataListener);
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    protected void compose(Component host) {
        if (getEnd() < getBegin()) {
            throw new IllegalArgumentException("End index must not be less than start index.");
        }
        Object items = getItems();
        Template tm = getTemplate("");
        Object value = _conv.coerceToUi(items);

        if (tm != null) {
            List<?> list = null;
            if (value != null && value instanceof List) {
                list = (List<Object>) value;
            }
            // support list model
            boolean isUsingListModel = items instanceof ListModel;
            if (isUsingListModel) {

                // Bug ZK-2837
                initDataListener(((ListModel<?>) items), host);

                String forEachRenderedCompAttr = TemplateBasedShadowElement.FOREACH_RENDERED_COMPONENTS
                        + this.getUuid();
                if (host.hasAttribute(forEachRenderedCompAttr)) {
                    //clear
                    Object shadowInfo = ShadowElementsCtrl.getCurrentInfo();
                    try {
                        ShadowElementsCtrl.setCurrentInfo(this);
                        host.removeAttribute(forEachRenderedCompAttr);
                        for (ListIterator<Component> l = this.getDistributedChildren().listIterator(); l.hasNext(); ) {
                            l.next();
                            l.remove();
                        }
                    } finally {
                        ShadowElementsCtrl.setCurrentInfo(shadowInfo);
                    }
                }
            }
            try {
                ForEachRenderer renderer = (ForEachRenderer) Class.forName("cn.easyplatform.web.task.api.ForEachRenderer").newInstance();
                renderer.render(this, host, tm, list, isUsingListModel);
            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                throw new UiException("Wrong ForEachRenderer");
            }
        }
    }

    public boolean isDynamicValue() {
        if (getItems() instanceof ListModel || super.isDynamicValue())
            return true;
        return false;
    }

    //--ComponentCtrl--//
    private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(6);

    static {
        _properties.put("items", new ObjectPropertyAccess() {
            public void setValue(Component cmp, Object value) {
                ((ForEach) cmp).setItems(value);
            }

            public Object getValue(Component cmp) {
                return ((ForEach) cmp).getItems();
            }
        });
        _properties.put("begin", new IntPropertyAccess() {
            public void setValue(Component cmp, Integer value) {
                ((ForEach) cmp).setBegin(value);
            }

            public Integer getValue(Component cmp) {
                return ((ForEach) cmp).getBegin();
            }
        });
        _properties.put("end", new IntPropertyAccess() {
            public void setValue(Component cmp, Integer value) {
                ((ForEach) cmp).setEnd(value);
            }

            public Integer getValue(Component cmp) {
                return ((ForEach) cmp).getEnd();
            }
        });
        _properties.put("step", new IntPropertyAccess() {
            public void setValue(Component cmp, Integer value) {
                ((ForEach) cmp).setStep(value);
            }

            public Integer getValue(Component cmp) {
                return ((ForEach) cmp).getStep();
            }
        });
        _properties.put("var", new StringPropertyAccess() {
            public void setValue(Component cmp, String value) {
                ((ForEach) cmp).setVar(value);
            }

            public String getValue(Component cmp) {
                return ((ForEach) cmp).getVar();
            }
        });
        _properties.put("varStatus", new StringPropertyAccess() {
            public void setValue(Component cmp, String value) {
                ((ForEach) cmp).setVarStatus(value);
            }

            public String getValue(Component cmp) {
                return ((ForEach) cmp).getVarStatus();
            }
        });
    }

    public PropertyAccess getPropertyAccess(String prop) {
        PropertyAccess pa = _properties.get(prop);
        if (pa != null)
            return pa;
        return super.getPropertyAccess(prop);
    }

    @Override
    public void removeFromParent() {
        Object items = getItems();
        if (items instanceof ListModel) {
            ((ListModel<?>) items).removeListDataListener(_dataListener);
        }
        super.removeFromParent();
    }
}
